package org.hive.testserver.domain.model.schedule;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.collections.ComparatorUtils;
import org.apache.commons.collections.TransformerUtils;
import org.apache.commons.lang.SerializationUtils;
import org.apache.commons.lang.StringUtils;
import org.hive.testserver.domain.model.BaseWork;
import org.hive.testserver.domain.model.ExecuteTest;
import org.hive.testserver.domain.model.Slave;
import org.hive.testserver.domain.model.Test;
import org.hive.testserver.domain.model.TestState;
import org.hive.testserver.domain.model.Work;
import org.hive.testserver.domain.model.WorkSheet;

/**
 * Exclude allocating tests to a slave that has repeatedly failed in execution of tests under the assumption that
 * the slave has fouled itself or its environment and is not able to successfully run any further tests.
 * This signal may actually indicate that lots of tests are simply bad, so attempt to protect against that
 * misdiagnosis.
 */
public class QuarantineRogueSlaveAllocator implements AllocatorLink, Serializable {

	private AllocatorLink next;

	private int consecutiveFailedTestTrigger;
	private transient Map<String,Boolean> functioningSlavesByName;
	
	private static final long serialVersionUID = 1L;
	
	private static final int CONSECUTIVE_FAILED_TEST_TRIGGER = 15;

	public QuarantineRogueSlaveAllocator(AllocatorLink next) {
		this(CONSECUTIVE_FAILED_TEST_TRIGGER, next);
	}
	
	public QuarantineRogueSlaveAllocator(int consecutiveFailedTestTrigger, AllocatorLink next) {
		super();
		this.next = next;
		this.consecutiveFailedTestTrigger = consecutiveFailedTestTrigger;
	}

	private Map<String,Boolean> slaveCategories() {
		if (functioningSlavesByName == null) {
			functioningSlavesByName = new HashMap<String,Boolean>();
		}
		return functioningSlavesByName;
	}

	public boolean accept(Test test, Slave slave, WorkSheet workSheet) {
		if (functioningSlave(slave, workSheet) && next != null) {
			return next.accept(test, slave, workSheet);
		} else {
			return false;
		}
	}


	private boolean functioningSlave(Slave slave, WorkSheet workSheet) {
		Boolean functioning = slaveCategories().get(slave.getName());
		if (functioning == null) {
			functioning = calculateWhetherSlaveIsFunctioning(slave, workSheet);
			slaveCategories().put(slave.getName(), functioning);
		}
		return functioning;
	}

	@SuppressWarnings("unchecked")
	private boolean calculateWhetherSlaveIsFunctioning(Slave slave, WorkSheet workSheet) {
		ArrayList<Work> works = new ArrayList<Work>(workSheet.worksForSlave(slave));
		Collections.sort(works, ComparatorUtils.transformedComparator(ComparatorUtils.NATURAL_COMPARATOR, TransformerUtils.invokerTransformer(BaseWork.STARTED_ACCESSOR)));
		int failures = 0;
		for (int i = works.size() - 1; i >= 0; --i) {
			Work work = works.get(i);
			//FIXME get away from type checking
			if (work instanceof ExecuteTest && work.isFinished()) {
				if (StringUtils.equals(TestState.PASSED.toString(), work.result())) {;
					break;
				} else {
					failures += 1;
				}
			}
		}
		return failures >= consecutiveFailedTestTrigger;
	}

	protected boolean shouldExecuteOnAlternativeSlave(Test test, Slave slave, WorkSheet workSheet) {
		return testHasAlreadyBeenExecutedBySlave(test, slave, workSheet) && workSheet.areThereAnyOtherActiveSlaves(slave);
	}

	private boolean testHasAlreadyBeenExecutedBySlave(Test test, Slave slave, WorkSheet workSheet) {
		if (test.timesExecuted() == 0) {
			return false;
		}
		List<Work> works = workSheet.findWorksForTest(test);
		for (Work work : works) {
			if (slave.equals(work.assignedSlave())) {
				return true;
			}
		}
		return false;
	}

	public AllocatorLink createWorkingCopy() {
		return (AllocatorLink) SerializationUtils.clone(this);
	}


	public ScheduledTest scheduleNextTest(Slave slave, WorkSheet workSheet) {
		if (functioningSlave(slave, workSheet)) {
			return next.scheduleNextTest(slave, workSheet);
		} else {
			return new ScheduledTest(null, ScheduleReason.SLAVE_QUARANTINED);
		}
	}


	public boolean shouldConsiderTestFinished(Test test) {
		return next.shouldConsiderTestFinished(test);
	}

}
